# Python趣味编程：第 4 章
#    游戏的扩展
# --------------------------
# 程序名: ex04-blocks.py

from tkinter import *
from dataclasses import dataclass
import random
import time

# =================================================
# 初始设定值
WALL_EAST = 800                 # 墙的东侧最大值(X最大)
WALL_SOUTH = 600                # 墙的南侧最大值(Y最大)

VX0 = [-3, -2, -1, 1, 2, 3]     # 球的x方向初速选项
BALL_X0 = WALL_EAST / 2         # 球的初始位置(x)
BALL_Y0 = 150                   # 球的初始位置(y)
BALL_D = 10                     # 球的大小
BALL_VX = random.choice(VX0)    # 球的x方向初速
BALL_VY = 3                     # 球的y方向初速
SPEED_UP = 10                   # 加速球的频率
BALL_MAX_VY = 10                # 球的最高速度
MULTI_BALL_COUNT = 4            # 分裂球的频率
BALL_MAX_NUM = 5                # 分裂球的最大数量

PADDLE_X0 = WALL_EAST / 2 - 50  # 推杆的初始位置(×)
PADDLE_Y0 = WALL_SOUTH - 100    # 推杆的初始位置(y)
PADDLE_W = 100                  # 推杆的宽度(w)
PADDLE_H = 20                   # 推杆的高度(h)
PADDLE_VX = 5                   # 推杆的速度
                                # 准备改变的颜色
PADDLE_COLORS = ["blue", "red", "green", "yellow", "brown", "gray"]
PADDLE_SHORTEN = 10             # 每次缩短推杆的量
PADDLE_SHORTEN_COUNT = 10       # 缩短推杆的命中次数
PADDLE_MIN_W = 20               # 推杆的最小宽度 

NUM_ROWS = 9         # x方向的块数
NUM_COLS = 3         # y方向的块数
BLOCK_X = 10         # 砖块的位置(x)
BLOCK_Y = 50         # 砖块的位置(y)
BLOCK_W = 80         # 砖块的宽度
BLOCK_H = 30         # 砖块的高度
BLOCK_PAD = 5        # 块之间(填充)
BLOCK_COLORS = ["green", "blue", "darkgray"] # 蓝色的颜色

CANDY_BONUS = 50
CANDY_W = 10
CANDY_H = 10

ADD_SCORE = 10                  # 得分的增加值
DURATION = 0.01                 # 绘图间隔(秒)

# -----------------------------------
@dataclass
class Ball:
    id: int
    x: int
    y: int
    vx: int
    vy: int
    d: int
    c: str

@dataclass
class Paddle:
    id: int
    x: int
    y: int
    w: int
    h: int
    vx: int
    c: str

@dataclass
class Block:
    id: int
    x: int
    y: int
    w: int
    h: int
    pt: int
    bc: int
    c: str

@dataclass
class Spear:
    id: int
    x: int
    y: int
    w: int
    h: int
    vy: int
    c: str

@dataclass
class Candy:
    id: int
    x: int
    y: int
    w: int
    h: int
    vy: int
    c: str

@dataclass
class Game:
    start: int
# -------------------------
# ball
# 球的绘制和定义
def make_ball(x, y, vx, vy, d=3, c="black"):
    id = canvas.create_oval(x, y, x + d, y + d, fill=c, outline=c)
    return Ball(id, x, y, vx, vy, d, c)

# 球的移动
def move_ball(ball):
    ball.x += ball.vx
    ball.y += ball.vy

# 球的重新绘制
def redraw_ball(ball):
    canvas.coords(ball.id, ball.x, ball.y,
                  ball.x + ball.d, ball.y + ball.d)

# -------------------------
# paddle
# 推杆的绘制和定义
def make_paddle(x, y, w=PADDLE_W, h=PADDLE_H, c="blue"):
    id = canvas.create_rectangle(x, y, x + w, y + h, fill=c, outline=c)
    return Paddle(id, x, y, w, h, 0, c)

# 推杆的移动(左右)
def move_paddle(pad):
    pad.x += pad.vx

# 改变车轴的颜色
def change_paddle_color(pad, c="red"):
    canvas.itemconfigure(pad.id, fill=c)
    canvas.itemconfigure(pad.id, outline=c)
    redraw_paddle(pad)

# 推杆的重绘
def redraw_paddle(pad):
    canvas.coords(pad.id, pad.x, pad.y,
                  pad.x + pad.w, pad.y + pad.h)

# ------------------
# block
# 砖块的绘制和定义
def make_block(x, y, w=120, h=40, pt=10, bc=1, c="green"):
    id = canvas.create_rectangle(x, y, x + w, y + h, fill=c, outline=c)
    return Block(id, x, y, w, h, pt, bc, c)

# 消除步骤
def delete_block(block):
    canvas.delete(block.id)

# 生成多个序列
def make_blocks(n_rows, n_cols, x0, y0, w, h, pad, colors):
    blocks = []
    x0_save = x0
    for y in range(n_cols):
        x0 = x0_save
        bc = n_cols - y
        pt = 10 * bc
        for x in range(n_rows):
            blocks.append(make_block(x0, y0, w, h, pt, bc,
                                     colors[bc - 1]))
            x0 = x0 + w + pad
        y0 = y0 + h + pad
    return blocks

# ------------------
# spear
# 标枪的绘制和定义
def make_spear(x, y, w=1, h=40, vy=5, c="red"):
    id = canvas.create_rectangle(x, y, x + w, y + h,
                                 fill=c, outline=c)
    return Spear(id, x, y, w, h, vy, c)

# 把枪弄灭
def delete_spear(spear):
    canvas.delete(spear.id)

# 枪的移动(上下)
def move_spear(spear):
    spear.y += spear.vy

# 枪的重绘
def redraw_spear(spear):
    canvas.coords(spear.id, spear.x, spear.y,
                  spear.x + spear.w, spear.y + spear.h)

# ------------------
# candy(奖励条款)
# 糖果的绘制和定义
def make_candy(x, y, w=5, h=5, vy=5, c="green"):
    id = canvas.create_rectangle(x, y, x + w, y + h,
                                 fill=c, outline=c)
    return Candy(id, x, y, w, h, vy, c)

# 把糖果熄灭
def delete_candy(candy):
    canvas.delete(candy.id)

# 移动糖果(上下)
def move_candy(candy):
    candy.y += candy.vy

# 糖果的重新绘制
def redraw_candy(candy):
    canvas.coords(candy.id, candy.x, candy.y,
                  candy.x + candy.w, candy.y + candy.h)

# -------------------------
# wall
# 墙的生成
def make_walls(ox, oy, width, height):
    canvas.create_rectangle(ox, oy, ox + width, oy + height)

# ------------------
# 等待SPACE Key
def game_start(event):
    game.start = True

def game_over():
    canvas.create_text(WALL_EAST/2, 200, text="Game Over!",
                       font = ('FixedSys', 16))

# -------------------------
# 推杆操作的事件处理
def left_paddle(event):        # 将速度设定为向左(负)
    paddle.vx = -PADDLE_VX

def right_paddle(event):       # 将速度设定为向右(正)
    paddle.vx = PADDLE_VX

def stop_paddle(event):        # 将速度设定为零
    paddle.vx = 0

# =================================================
# 初始设置
tk = Tk()
tk.title("Game")

canvas = Canvas(tk, width=WALL_EAST, height=WALL_SOUTH, bd=0,
                highlightthickness=0)
canvas.pack()
tk.update()

game = Game(False)

score = 0           # 得点
spear = None
candy = None
paddle_count = 0    # 用推杆击球的次数
balls = []          # 球的排列

# 准备绘图道具
make_walls(0, 0, WALL_EAST, WALL_SOUTH)
paddle = make_paddle(PADDLE_X0, PADDLE_Y0)
balls.append(make_ball(BALL_X0, BALL_Y0, BALL_VX, BALL_VY, BALL_D))
blocks = make_blocks(NUM_ROWS, NUM_COLS, BLOCK_X, BLOCK_Y,
                     BLOCK_W, BLOCK_H, BLOCK_PAD, BLOCK_COLORS)

# 连接事件和事件处理程序
canvas.bind_all('<KeyPress-Left>', left_paddle)
canvas.bind_all('<KeyPress-Right>', right_paddle)
canvas.bind_all('<KeyRelease-Left>', stop_paddle)
canvas.bind_all('<KeyRelease-Right>', stop_paddle)
canvas.bind_all('<KeyPress-space>', game_start)  # SPACE被按下了

# -------------------------
# 分数的表示
id_score = canvas.create_text(
    10, 10, text=("score: " + str(score)), font=("FixedSys", 16),
    justify="left", anchor=NW
    )

# 等待输入SPACE
id_text = canvas.create_text(
    WALL_EAST/2, 200, text="Press 'SPACE' to start",
    font=('FixedSys', 16)
    )

tk.update()

while not game.start:    # 一味地等待SPACE
    tk.update()
    time.sleep(DURATION)

canvas.delete(id_text)  # 删除输入SPACE的信息
tk.update()

# -------------------------
# 程序的主循环
while True:
    move_paddle(paddle)       # 车轴的移动
    if spear:
        move_spear(spear)     # 标枪的下落
    if candy:
        move_candy(candy)     # 糖果的掉落
    for ball in balls:
        move_ball(ball)       # 球的移动
        if ball.x + ball.vx <= 0:  # 在左墙上反弹
            ball.vx = - ball.vx
        if ball.x + ball.d + ball.vx  >= WALL_EAST: # 右边的墙
            ball.vx = - ball.vx
        if ball.y + ball.vy <= 0:  # 上边的墙
            ball.vy = - ball.vy
        if ball.y + ball.d + ball.vy >= WALL_SOUTH : # 移到下面去了
            canvas.delete(ball.id)   # 把球从画面上抹掉
            balls.remove(ball)
    if len(balls)==0:   # 错过了最后一个球
        game_over()
        break
    if spear:
        if (paddle.x <= spear.x <= paddle.x + paddle.w \
            and spear.y + spear.h > paddle.y \
            and spear.y <= paddle.y + paddle.h):  # 中了枪
            redraw_paddle(paddle)
            redraw_spear(spear)
            game_over()
            break
    if candy:
        if (paddle.x <= candy.x <= paddle.x + paddle.w \
            and candy.y + candy.h > paddle.y \
            and candy.y <= paddle.y + paddle.h):  # 获得奖金
            score += CANDY_BONUS
            canvas.itemconfigure(id_score, text="score:" + str(score))
            delete_candy(candy)
            candy = None

    for ball in balls:
        # 球的下方触到推杆的上面，横向位置与推杆重叠
        if (paddle.y <= ball.y + ball.d <= paddle.y + paddle.h \
            and paddle.x < ball.x + ball.d/2 < paddle.x + paddle.w):
            change_paddle_color(paddle, random.choice(PADDLE_COLORS))
            ball.vy = -ball.vy    # 球的移动方向改变
            # 移动后仍与推杆重叠:从侧面重叠时
            if paddle.y <= ball.y + ball.d + ball.vy <= paddle.y + paddle.h:
                ball.y = paddle.y - ball.d # 回到推杆上
            paddle_count += 1
            if paddle_count % PADDLE_SHORTEN_COUNT == 0: # 是把推杆变短?？
                if paddle.w > PADDLE_MIN_W:  # 还可以短一点!
                    paddle.w -= PADDLE_SHORTEN
            if paddle_count % SPEED_UP == 0: # 让球加速吗？
                if ball.vy > - BALL_MAX_VY: # 还可以加速！
                    ball.vy -= 1   # 注意球是朝上的！
            # 根据球的位置改变反射角度
            ball.vx = int(6 * (ball.x + ball.d/2 - paddle.x)
                             / paddle.w) - 3
            if paddle_count % MULTI_BALL_COUNT == 0: # 产生球
                if len(balls) < BALL_MAX_NUM:
                    balls.append(
                        make_ball(
                            BALL_X0, BALL_Y0, random.choice(VX0),
                            BALL_VY, BALL_D
                            ))

        for block in blocks: # 对所有的模块进行调查
            # 球的x位置在砖块范围内，球的Y位置在砖块范围内
            if (block.x < ball.x + ball.d/2 < block.x + block.w \
                and (block.y <= ball.y <= block.y + block.h
                     or block.y <= ball.y + ball.d \
                         <= block.y + block.h)):
                ball.vy = -ball.vy
                block.bc -= 1
                if block.bc == 0:  # 剩余硬度为0
                    score += block.pt  # 每局得分不同
                    canvas.itemconfigure(id_score, text="score:" + str(score))
                    delete_block(block)
                    blocks.remove(block)
                    break
                else:   # 用砖块的颜色表示硬度
                    canvas.itemconfigure(block.id,
                                         fill=BLOCK_COLORS[block.bc - 1])
                    canvas.itemconfigure(block.id,
                                         outline=BLOCK_COLORS[block.bc - 1])

    if len(blocks) == 0:  # 序列为空的另一种检查方法
        canvas.create_text(WALL_EAST/2, 200,
                           text="Clear!", font=('FixedSys', 16))
        break

    if spear==None and random.random() < 0.01:  # 1%的概率发生
        spear = make_spear(random.randint(100, WALL_EAST - 100), 10)
    if spear and spear.y + spear.h >= WALL_SOUTH:
        delete_spear(spear)
        spear = None

    if candy==None and random.random() < 0.005:
        candy = make_candy(random.randint(100, WALL_EAST - 100), 10,
                           CANDY_W, CANDY_H)
    if candy and candy.y + candy.h >= WALL_SOUTH:
        delete_candy(candy)
        candy = None

    redraw_paddle(paddle)        # 推杆的重绘
    for ball in balls:
        redraw_ball(ball)     # 球的重新绘制
    if spear:
        redraw_spear(spear)   # 标枪的重新绘制
    if candy:
        redraw_candy(candy)   # 糖果的重新绘制
    tk.update()               # 绘图反映在画面上
    time.sleep(DURATION)      # 在接下来的绘图之前，都要sleep
